package bancosys.tec.persist.traverse;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.hibernate.EntityMode;
import org.hibernate.Hibernate;
import org.hibernate.SessionFactory;
import org.hibernate.engine.CascadeStyle;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.persister.entity.AbstractEntityPersister;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.type.Type;

import bancosys.tec.persist.Persistable;
import bancosys.tec.persist.persister.DefaultPersister;
import bancosys.tec.utils.traverser.AbstractTraverser;
import bancosys.tec.utils.traverser.Visitor;

/**
 * Classe que representa um traverser de persistables
 * 
 * @author pedro
 */
public class PersistableTraverser extends AbstractTraverser {
    private SessionFactory sessionFactory;

    private static final List<Object> EMPTY_LIST = new LinkedList<Object>();

    /**
     * Classe que percorre a estrutura do grafo de objetos
     * 
     * @param visitors lista de visitors a serem aplicados
     */
    public PersistableTraverser(List<? extends Visitor> visitors) {
        super(visitors);
    }

    /**
     * Devolve a {@link SessionFactory}
     * 
     * @return a {@link SessionFactory}
     */
    public SessionFactory getSessionFactory() {
        return this.sessionFactory;
    }

    /**
     * Define a {@link SessionFactory}
     * 
     * @param sessionFactory {@link SessionFactory}
     */
    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    /**
     * Devolve um Iterable de objetos relacionados com o nó.
     * 
     * @param node nó que está sendo visitado
     * @return um Iterable de objetos relacionados
     */
    @Override
    protected Iterable<Object> getLinkedNodes(Object node) {
        if (!Hibernate.isInitialized(node)) {
            return EMPTY_LIST;
        }

        Collection<Object> linkedNodes;
        if (this.isCollectionType(node)) {
            linkedNodes = this.extractCollectionElements(node);
        } else {
            linkedNodes = this.extractLinkedNodes(node);
        }

        return linkedNodes;
    }

    /**
     * Devolve os objetos relacionados com cascade do nó passado.
     * 
     * @param node nó
     * @return coleção
     */
    private Collection<Object> extractLinkedNodes(Object node) {
        Collection<Object> linkedNodes = new ArrayList<Object>();

        ClassMetadata classMetadata = this.getSessionFactory().getClassMetadata(Hibernate.getClass(node));
        String[] properties = classMetadata.getPropertyNames();

        for (int i = 0; i < properties.length; i++) {
            String property = properties[i];
            Type type = classMetadata.getPropertyType(property);
            Object resolvedObject = this.narrow(node);
            Object value = classMetadata.getPropertyValue(resolvedObject, property, EntityMode.POJO);

            CascadeStyle cascadeStyle = ((AbstractEntityPersister) classMetadata).getCascadeStyle(i);
            if (!type.isAssociationType() || value == null
                    || !DefaultPersister.isCascadeSave(cascadeStyle)) {
                continue;
            }

            linkedNodes.add(value);

        }

        return linkedNodes;
    }

    /**
     * Devolve os elementos da coleção se node for uma coleção ou uma lista vazia caso não seja uma coleção
     * 
     * @param node nó
     * @return coleção que o nó representa
     */
    @SuppressWarnings("unchecked")
    private Collection<Object> extractCollectionElements(Object node) {
        Collection elements = new ArrayList();

        if (node instanceof Collection) {
            elements = (Collection) node;

        } else if (node instanceof Map) {
            Map map = (Map) node;
            elements.addAll(map.keySet());
            elements.addAll(map.values());

        } else if (node.getClass().isArray()) {
            for (int i = 0; i < Array.getLength(node); i++) {
                elements.add(Array.get(node, i));
            }
        }

        return elements;
    }

    /**
     * Resolve o proxy de um objeto se necessario.
     * 
     * @param original objeto persistido, pode estar com lazy ou não
     * @return instancia real do objeto.
     */
    // TODO Em busca de solução mais limpa. Código replicado do BaseDAO.
    private Object narrow(Object original) {
        Object resolvedObject;
        if (original == null) {
            resolvedObject = null;
        }

        if (original instanceof HibernateProxy) {
            resolvedObject = ((HibernateProxy) original).getHibernateLazyInitializer().getImplementation();
        } else {
            resolvedObject = original;
        }
        return resolvedObject;
    }

    /**
     * Método que diz se deve parar a busca nesse ponto ou não. Apenas devolve <code>true</code> se o nó já foi visitado ou se não é um
     * persistable
     * 
     * @param node nó que deve ser visitado ou não
     * @param visitedNodes conjunto de nós que já foram visitados
     * @return se deve parar a busca nesse ponto ou não
     */
    @Override
    protected boolean mustStopTraverse(Object node, Set<Object> visitedNodes) {
        if (super.mustStopTraverse(node, visitedNodes)) {
            return true;
        }

        return !(this.isCollectionType(node) || (node instanceof Persistable));
    }

}
